<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Palanthir BV</title><link href="https://palanthir.nl/" rel="alternate"></link><link href="https://palanthir.nl/Atom%20feed" rel="self"></link><id>https://palanthir.nl/</id><updated>2025-11-24T00:00:00+01:00</updated><entry><title>Charging electric vehicle without the cloud</title><link href="https://palanthir.nl/charging-electric-vehicle-without-the-cloud-en.html" rel="alternate"></link><published>2025-11-24T00:00:00+01:00</published><updated>2025-11-24T00:00:00+01:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2025-11-24:/charging-electric-vehicle-without-the-cloud-en.html</id><summary type="html">&lt;p&gt;My wife and I both drive electric vehicles (EV) for years now. More than 6 years ago we got our first EV for my company.
To be able to charge it at home and be able to declare the charging sessions to my company we got a charger from EVBox …&lt;/p&gt;</summary><content type="html">&lt;p&gt;My wife and I both drive electric vehicles (EV) for years now. More than 6 years ago we got our first EV for my company.
To be able to charge it at home and be able to declare the charging sessions to my company we got a charger from EVBox
including a corresponding subscription which handled the invoicing of the charging sessions to my company. The cloud software
from EVBox handled the invoicing of each charging session to my company, and paid out the money to my private bank account 
since the electricity loading into the car comes from our private home electrical connection to the grid.&lt;/p&gt;
&lt;p&gt;This worked fine for a couple of years.  &lt;/p&gt;
&lt;p&gt;We had a business line charger with a mobile 3G connection. 2 years ago the mobile network providers in the Netherlands decided
to phase out 3G. I never received the email from EVBox that our charger needed to be upgraded to function after 2025.
Upgrade kits were sold until July 2024, costing about € 400, and by accident I found out about this necessary upgrade months 
after that.&lt;/p&gt;
&lt;p&gt;Then in the fall of 2024 EVBox announced to completely stop with their charger business. From December 1st 2025, our charger
would function only as a regular home charger without any registration of charging sessions whatsoever. There are other
cloud payment providers that you can switch to to keep the full functionality, but I received an email earlier this year
that this was not the case for our charger. It seems our charger did not have the firmware to be able to switch to another
provider.&lt;/p&gt;
&lt;p&gt;Bummer.  &lt;/p&gt;
&lt;p&gt;So, basically we needed a new charger. We have used the payment services of EVBox for over 6 years now and we are stuck
with an e-waste charger, forced to buy a new charger and everybody is saying 'just switch to another provider'. I know almost
everything works now only when connected to the cloud, but in this case I am tired of paying for a subscription that only
sends an invoice every month and handles the bank transactions.&lt;/p&gt;
&lt;h2&gt;Learning about OCPP&lt;/h2&gt;
&lt;p&gt;So I started to shop for a new car charger that I could manage myself without any cloud service. In the car charging world,
there is a standard for exactly this purpose: OCPP, Open Charge Point Protocol. Most chargers you can buy nowadays have this,
but it is tricky. There are a lot of car chargers available that support OCPP, but only via the cloud of their manufacturer.
If I want to be able to manage my new car charger myself, I need a device that can be setup to use OCPP directly.&lt;/p&gt;
&lt;p&gt;So I went shopping and found the &lt;a href="https://help.zaptec.com/hc/article_attachments/33332948823825"&gt;Zaptec Go&lt;/a&gt;. In the
specification it says clearly "OCPP 1.6J cloud-to-cloud or native on-device". Exactly what I needed, the smallest size I 
could find, and the cheapest one, so I ordered it.&lt;/p&gt;
&lt;p&gt;To manage a car charger that can be managed by OCPP, I also needed some back-end software and found that in the community
open source software &lt;a href="https://github.com/steve-community/steve"&gt;Steve&lt;/a&gt;.&lt;br&gt;
With this software we can manage the car charger, manage which NFC tags are authorized to start a charging session, and it
keeps track of the charging sessions.&lt;/p&gt;
&lt;h2&gt;Installing the new car charger&lt;/h2&gt;
&lt;p&gt;So I ordered the &lt;a href="https://www.zaptec.com/charging-solutions/home-charging/zaptec-go"&gt;Zaptec Go&lt;/a&gt; and a week ago I had
everything prepared to make the switch.&lt;/p&gt;
&lt;p&gt;It took me 2,5 hours to disassemble the EVBox BusinessLine charger, that (big) device is really not made for that. In the end
I used a jigsaw to be able to remove the cover of it.&lt;/p&gt;
&lt;p&gt;Putting the Zaptec Go on the wall was done in 30 minutes. Of course I had the benefit that the cable to the charger was 
already present, so it was just putting 4 screws in the wall and connect the wires.
&lt;img alt="Zaptec Go on the wall of our home" src="https://palanthir.nl/images/zaptec-go-home.jpg"&gt;&lt;/p&gt;
&lt;h2&gt;Configuration of the charger&lt;/h2&gt;
&lt;p&gt;Nice, now we can start setting up the new charger! I already suspected this would not be a simple task, and I was right.
Setting the charger up with a bluetooth connection in their app was simple. After that, the charger was connected to my
guest WiFi network which I also use for devices like this that do not belong on our own network. Zaptec even offers a
more extensive web portal to manage your charger from &lt;a href="https://portal.zaptec.com/"&gt;there&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;Of course, by default the charger connects to the cloud of the manufacturer, so although I could see the charger would work,
I could not start a charging session yet because I had no RFID/NFC tag authorized yet. You can start a charging session
directly from the mobile app also, but then I had to find out how to export charging sessions from the Zaptec portal, and the
main goal was to implement a solution that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Stays local and works, even when our internet connection is down&lt;/li&gt;
&lt;li&gt;Keeps local track of our charging sessions, so we can&lt;/li&gt;
&lt;li&gt;Automatically send an invoice each month for the charging sessions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So I started reading the documentation from &lt;a href="https://docs.zaptec.com/docs/zaptec-go-ocpp16j-configuration-guide"&gt;Zaptec&lt;/a&gt;. It
mentioned something about user permissions which I could not find when I was logged into their portal, and basically
it describes to enable OCPP as follows:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Authentication types of charger in the Zaptec portal" src="https://palanthir.nl/images/zaptec-portal-installation-authentication.png"&gt;&lt;/p&gt;
&lt;p&gt;I had the first 3 options, but not the last one, exactly the one I needed. My own install of the Steve software was not
publicly accessible from the Internet (obviously for security reasons), so I could not get it to work.&lt;/p&gt;
&lt;p&gt;The URL of my Steve installation was &lt;code&gt;https://steve.int.palanthir.nl&lt;/code&gt;, notice the &lt;code&gt;int.palanthir.nl&lt;/code&gt; domain name,
which is not accessible from the Internet.&lt;/p&gt;
&lt;p&gt;By now it was Saturday evening about 11 PM...  &lt;/p&gt;
&lt;p&gt;I realized I needed support from Zaptec, so I sent a support request to them through their website, not expecting much of it.
Especially during the weekend I do not expect support from a manufacturer to be available and respond to me.&lt;/p&gt;
&lt;p&gt;After this, I spent some hours reconfiguring my network and opening ports in my firewall to have the Steve software available from
the Internet in a somewhat secure way. I got a working setup with their 'OCPP 1.6J Cloud' authentication type and the following
URL of my Steve OCPP management software:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;wss://steve.palanthir.nl/steve/websocket/CentralSystemService/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Although I now had a working setup, it was still using the cloud, not an option for me. With all the connections
working, I could now write a script to retrieve the charging sessions from the Steve software and create an invoice in my 
&lt;a href="https://invoiceplane.com"&gt;invoicing software&lt;/a&gt;. &lt;/p&gt;
&lt;h2&gt;Zaptec support&lt;/h2&gt;
&lt;p&gt;On Monday afternoon I received an email from Zaptec support that OCPP was not implemented in the Netherlands yet, but
they were trying to find out how to help me. It took about 4 days of emailing back and forth until I had the option to 
configure the direct OCPP variant of their authentication type named "OCPP 1.6J". This profile was needed to get it working.&lt;/p&gt;
&lt;p&gt;But it didn't. Like above, I set the OCPP websocket URL to this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;wss://steve.int.palanthir.nl/steve/websocket/CentralSystemService/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I now suspected some DNS problem or whatever, so I started digging in the logs of the Steve software to find out what was
going wrong. By default, Steve is configured with the INFO log level, which did not give me any clue. So I reconfigured
that to the DEBUG log level, which gives &lt;em&gt;a lot&lt;/em&gt; of debug logging. After some digging, I found out the authorization of
the car charger to Steve was not working because the URL the car charger now was using to authenticate itself was &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;wss://steve.int.palanthir.nl/steve/websocket/CentralSystemService//&amp;lt;device&lt;span class="w"&gt; &lt;/span&gt;serial&lt;span class="w"&gt; &lt;/span&gt;number&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice the 2 slashed in the URL? That was the culprit. When I set the Websocket URL in the Zaptec portal to&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;wss://steve.int.palanthir.nl/steve/websocket/CentralSystemService
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;without the trailing slash character, it worked within seconds!&lt;/p&gt;
&lt;p&gt;I tested this configuration also by disconnecting the Internet connection at home, and the car charger kept working and
authenticating by using the NFC cards I authorized for charging.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I understand that for a manufacturer the most easy way to configure their devices is to have a cloud service available 
for that. This makes the firmware in the devices smaller, and their cloud software is easier to maintain and up to date.
I am very grateful that manufacturers like Zaptec still exist to make their devices work without an Internet connection.&lt;/p&gt;
&lt;p&gt;And I am even more grateful that I now have a setup that does not require a payment provider with a subscription to handle
12 invoices a year and pay for it.&lt;/p&gt;
&lt;p&gt;The downsides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Steve software does not look as modern as mobile apps or a modern web portal looks like.&lt;/li&gt;
&lt;li&gt;When I need to change settings in the car charger, I need to do it through the Steve software, the mobile app from Zaptec does not show my car charger anymore.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No subscription needed any more. I have a monthly Python script now that takes care of the invoicing.&lt;/li&gt;
&lt;li&gt;If Zaptec in the future decides not to support my charger any more, it keeps working.&lt;/li&gt;
&lt;li&gt;Even if our home Internet connection goes down, I still can start and stop charging sessions.&lt;/li&gt;
&lt;li&gt;We now have a solution which is extendable for the future, for example when we will have a home battery and do even more 
smart charging than the Zaptec portal can provide.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lots of thanks to the support team at Zaptec for giving me access to the correct authentication profile.&lt;/p&gt;</content><category term="home automation"></category><category term="electric vehicle"></category><category term="charger"></category><category term="ocpp"></category><category term="steve"></category><category term="ev"></category></entry><entry><title>Time ranges in postgresql with GIST indexes</title><link href="https://palanthir.nl/time-ranges-in-postgresql-with-gist-indexes-en.html" rel="alternate"></link><published>2025-09-16T00:00:00+02:00</published><updated>2025-09-16T00:00:00+02:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2025-09-16:/time-ranges-in-postgresql-with-gist-indexes-en.html</id><summary type="html">&lt;p&gt;At &lt;a href="https://coloclue.net"&gt;ColoClue&lt;/a&gt; we need a database which registers the resources members of the organization use. For example, we need to register which switch port on a network switch is being used by a member. This also counts for power bar ports, rackspace in a rack, access cards, cross connect links …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At &lt;a href="https://coloclue.net"&gt;ColoClue&lt;/a&gt; we need a database which registers the resources members of the organization use. For example, we need to register which switch port on a network switch is being used by a member. This also counts for power bar ports, rackspace in a rack, access cards, cross connect links in patch panel, and more.&lt;br&gt;
Years ago we had often conflicts representing the contents of this database compared to the real world situation. ColoClue often suffered situations in which there were conflicting records in the database with overlapping time ranges for a specific switch port or power bar port. This cannot happen in the real world: in one switch port can physically go only one cable. These overlapping records meant that someone had to go to the data centre to check what the real situation was at that moment. This is far from ideal.&lt;/p&gt;
&lt;p&gt;Also, because invoicing the use of resources happens only once a month, ColoClue has to keep a history of mutations to the database to correctly invoice the use of resources. For example, when someone starts to use a new power bar port on the 2nd of the month and stops using that before the invoices for that month are created a month later, ColoClue still has to charge money for it. So, keeping track of history is needed in the database.&lt;/p&gt;
&lt;p&gt;This is like the classic book lending problem for libraries, which is a more generic example. When someone lends a book from a library, the library needs to register which person has a book. And not only that, only one person can lend that specific book for a specific period of time. And, also a library need to keep track of these interactions with the book.&lt;/p&gt;
&lt;p&gt;This can be done in a relational database with a table design like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;serial4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;int4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;int4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start_datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end_datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this table the combination with person and book can be registered. For each lending action, the start date and time are being registered, and when the book is returned, that data and time are registered as end date. When the &lt;code&gt;end_datetime&lt;/code&gt; is empty, this means the person is still lending the book.&lt;/p&gt;
&lt;p&gt;This is a simple table design for registering the lending process. However, what it does not do is preventing the software that uses this table to insert overlapping time ranges. So it is possible to have records in the database that overlap the ranges used. The obvious solution for this is to have constraints that prevent this. This general problem occurs in a lot of software development projects.&lt;/p&gt;
&lt;p&gt;Almost every database has the possibility to add check constraints to a table to prevent this. However, these constraints can often become quite complex for situations like this, especially when taking into account the &lt;code&gt;removal_dt&lt;/code&gt; column for soft deletes described in &lt;a href="https://palanthir.nl/tracking-history-in-database-data-en.html"&gt;my article about tracking history in a database&lt;/a&gt;. Also, they are difficult to debug. That is why with a lot of software projects the constraint checking for this behavior is written in the middleware layer of the software, especially when writing software that has to be database independent.&lt;/p&gt;
&lt;p&gt;At ColoClue, we however had the situation that the database was accessed from different sources. Regardless of that, the best solution is to have the check constraints implemented directly on the database itself. This assures time range overlapping is not possible at all times from regardless from which source the database is being modified.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostGreSQL has a very elegant solution for this: the &lt;a href="https://www.postgresql.org/docs/current/rangetypes.html"&gt;tstzrange&lt;/a&gt; column type in combination with the &lt;a href="https://www.postgresql.org/docs/current/gist.html"&gt;GiST indexes&lt;/a&gt; functionality.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GiST indexes are an extension to PostGreSQL, available for almost any PostGreSQL instance. GiST indexes are often used in databases using geographical databases. They can be enabled by executing the following once on the current database schema.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;extension&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;btree_gist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using this, the table design looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;serial4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;int4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;int4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tstzrange&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;removal_dt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gist_book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EXCLUDE&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GIST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removal_dt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The constraint added describes to check for it only when the &lt;code&gt;removal_dt&lt;/code&gt; column is empty. We do not need to check for soft deleted data, making the index faster. Further, the operators used for checking the constraint determine how each column is checked. For the &lt;code&gt;book_id&lt;/code&gt; column this is simple, it has to check (the &lt;code&gt;=&lt;/code&gt; operator) for non-conflicting books. The &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; basically checks for overlapping &lt;code&gt;during&lt;/code&gt; time ranges.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.postgresql.org/docs/current/rangetypes.html"&gt;tstzrange&lt;/a&gt; data type is specific for PostGreSQL. It is handled a bit different from standard data type while querying and inserting data. Because it is a time range, it uses parentheses and square brackets to indicate the start- and end times of the range. The parentheses or brackets indicate whether the lower and upper bounds are exclusive or inclusive. Inserting a person lending a book will look this this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tstzrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2025-09-01 12:00:00&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;person_id&lt;/th&gt;
&lt;th&gt;book_id&lt;/th&gt;
&lt;th&gt;during&lt;/th&gt;
&lt;th&gt;removal_dt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;134&lt;/td&gt;
&lt;td&gt;["2025-09-01 12:00:00+00",)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Now, if someone else tries to lend the book an hour later, and error occurs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tstzrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2025-09-01 13:00:00&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="n"&gt;P01&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;conflicting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;violates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exclusion&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;constraint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;gist_book_lending&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;2025-09-01 13:00:00+00&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;conflicts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;existing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;2025-09-01 12:00:00+00&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the first person returns the book, we handle that like this using the same lending date and time the person did lend the book by using &lt;code&gt;lower(during)&lt;/code&gt; which takes the lower part of the time range:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tstzrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2025-09-16 10:00:00&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;person_id&lt;/th&gt;
&lt;th&gt;book_id&lt;/th&gt;
&lt;th&gt;during&lt;/th&gt;
&lt;th&gt;removal_dt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;134&lt;/td&gt;
&lt;td&gt;["2025-09-01 12:00:00+00","2025-09-16 10:00:00+00")&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If now the second persons lends the book, the constraint still goes of if there is still an overlap in the time range. So this does not work:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tstzrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2025-09-16 08:59:59&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="n"&gt;P01&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;conflicting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;violates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exclusion&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;constraint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;gist_book_lending&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;2025-09-16 09:59:59+00&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;conflicts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;existing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;2025-09-01 12:00:00+00&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;2025-09-16 10:00:00+00&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But this works because the top of the time range is set with &lt;em&gt;until&lt;/em&gt; instead of &lt;em&gt;until and including&lt;/em&gt; the higher time stamp:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tstzrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2025-09-16 10:00:00&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These checks are exactly what is necessary for the book lending constraints problem, solved with only a few SQL commands.&lt;/p&gt;
&lt;h3&gt;Drawbacks&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GiST indexes are a little bit slower than standard B-tree indexes. I did not do a benchmark compared to writing the check constraints myself, but I never had any performance problems with it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Getting the start - and end timestamps of the values of the during column can be a little bit more complicated, especially when using an &lt;a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping"&gt;ORM&lt;/a&gt;. This can be easily solved by creating a view that separates the lower and upper time range values:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VIEW&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vw_book_lending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPPER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;removal_dt&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_lending&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vw_book_lending&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;person_id&lt;/th&gt;
&lt;th&gt;book_id&lt;/th&gt;
&lt;th&gt;start_timestamp&lt;/th&gt;
&lt;th&gt;end_timestamp&lt;/th&gt;
&lt;th&gt;removal_dt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;134&lt;/td&gt;
&lt;td&gt;2025-09-01 12:00:00.000 +0000&lt;/td&gt;
&lt;td&gt;2025-09-16 10:00:00.000 +0000&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;134&lt;/td&gt;
&lt;td&gt;2025-09-16 10:00:00.000 +0000&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Advanced PostgreSQL types are underrated, they provide strong capabilities.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When using GiST and exclusion constraint, you can enforce complex logic at the database level. The error messages returned even show the conflicting record so you do not have to go and search for it in the table contents.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;By enforcing the constraint on the database level, it does not matter anymore from which source code the database is accessed. Data integrity is always enforced.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content><category term="databases"></category><category term="sql"></category><category term="databases"></category><category term="history"></category><category term="orm"></category><category term="database design"></category><category term="postgresql"></category><category term="tstzrange"></category><category term="gist"></category><category term="constraint"></category></entry><entry><title>Concerns about computer programming with AI</title><link href="https://palanthir.nl/concerns-about-computer-programming-with-ai-en.html" rel="alternate"></link><published>2025-09-02T00:00:00+02:00</published><updated>2025-09-02T00:00:00+02:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2025-09-02:/concerns-about-computer-programming-with-ai-en.html</id><summary type="html">&lt;p&gt;A few months ago I decided to publish more content on this web site. When I published my first articles,
the first response I received about those was "so refreshing to read an actual personally written article
again, instead of generated by AI". I guess that is reality nowadays.&lt;/p&gt;
&lt;p&gt;In …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A few months ago I decided to publish more content on this web site. When I published my first articles,
the first response I received about those was "so refreshing to read an actual personally written article
again, instead of generated by AI". I guess that is reality nowadays.&lt;/p&gt;
&lt;p&gt;In 2023 Microsoft released CoPilot. First as general chat bot and later Microsoft integrated the functionality
into development environments like Visual Studio (Code). It started as an AI code assistant suggesting
improvements of the code a developer is writing, this later improved into Vibe coding products that can do a
lot of programming for you.&lt;br&gt;
Since then CoPilot, Cursor, Claude and other Vibe coding tools have been booming.&lt;/p&gt;
&lt;p&gt;Almost every review I read about coding using AI, often referred to as Vibe coding, started
with something like "I am not against Vibe coding" and then the pros and cons of using these tools.&lt;/p&gt;
&lt;p&gt;Of course I have played with some of the vibe coding tools to see how it performs, in 2024 my experience was
"promising but quite not there yet". I was unimpressed. However, things are moving fast in this space and earlier this
year I tried again to make a small pilot project. I can see the results are becoming much better nowadays. &lt;a href="https://youtu.be/7kQi2HQMz4s?si=U37FbuMfZZs32W51"&gt;People
without much understanding of how to code can get results that at least deliver code thats does what they want&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After reading &lt;a href="https://lucianonooijen.com/blog/why-i-stopped-using-ai-code-editors/"&gt;this article about someone stopped using AI code editors&lt;/a&gt;, my own reservations about using vibe coding were more confirmed.&lt;/p&gt;
&lt;h1&gt;Intellectual property&lt;/h1&gt;
&lt;p&gt;Almost every commercial company nowadays has some form of IT implemented in their company. This can be using
only standard software, and often with (at least partly) custom programming and databases. &lt;strong&gt;This code and the content
of these databases are the intellectual property of the business.&lt;/strong&gt;&lt;br&gt;
Often that is their unique value compared to competitors. For example, if a company uses some form of customer relation management software, the contents of the database of that software, all their clients and information about their clients is stored there.&lt;/p&gt;
&lt;p&gt;This is very valuable information. I cannot imagine any company wants to give that information away and &lt;em&gt;even pay
for it&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In my professional environment I could not use vibe coding tools yet: when using AI assisted coding like like CoPilot
or Claude, (snippets of) the code you write are sent to the AI provider to train their AI model(s). When I write code
for my clients, usually the intellectual property rights are owned by the client. So when Microsoft started to push
their CoPilot product in almost every product, I had to be sure no client code was going to leak into the AI training
of CoPilot or whatever other AI assisted coding tool.&lt;br&gt;
I am not in the position to make this decision for the companies I consult.&lt;/p&gt;
&lt;p&gt;In the beginning of 2024 it was easy to do by disabling CoPilot in &lt;a href="https://code.visualstudio.com"&gt;Microsoft Visual Code&lt;/a&gt;,
but in the beginning of this year Microsoft started to push CoPilot so much that I could not trust anymore that no
code I was writing for my clients was leaking to Microsoft, so I switched to &lt;a href="https://vscodium.com"&gt;VSCodium&lt;/a&gt; which is
the same programming environment without all the Microsoft telemetry.&lt;/p&gt;
&lt;p&gt;Not only do the vibe coding tools ask for your code, they now also want to have access to the database the code has
to work with. I can understand that in a way, because most software only works perfectly in cooperation with their
corresponding databases.&lt;br&gt;
In a perfect world all companies and everybody only use mockup test data within their databases and keep production
data in a complete separate environment of course, but be real, this often does not happen. &lt;em&gt;It is a huge risk&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In the past a lot of programmers used tutorials to implement several services like AWS S3. Every tutorial says
"for this demo we do not implement credentials of this service, but you should do it for your production project"
and of course there were a &lt;a href="https://www.bitdefender.com/en-us/blog/businessinsights/worst-amazon-breaches"&gt;lot of data leaks of data stored in AWS S3 buckets&lt;/a&gt; that should have never happened.&lt;br&gt;
These security leaks still happen.&lt;/p&gt;
&lt;p&gt;Consider the following scenario: you have an existing project and want to use to AI to make improvements on it.
To accomplish this, you have to give the AI access to your code and database to make it understand on what to make
improvements. Even if you use a database with mockup data, the AI still has access to your code and trains itself
from it.&lt;br&gt;
And what happens when there is a bug in the production environment with production data not covered in your
mockup database? We are all humans, it could be faster to give the AI access to that database in the production
environment too. And by a simple small configuration error you gave the AI provider all of your data.&lt;/p&gt;
&lt;p&gt;The cause of the data breach is then basically a user error, not the AI, but I think we just have to wait until such data breaches happen. And if your data is in the hands of the AI company, &lt;a href="https://www.culawreview.org/ddc-x-culr-1/nyt-v-openai-and-microsoft"&gt;good luck with that&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Creativity&lt;/h1&gt;
&lt;p&gt;Large Language Models (LLM) are very impressively good in recognizing patterns. It is trained on A LOT of data and
based on that it can predict what mostly will be the next word it has present to you. Every LLM has a lot of
parameters where it can be 'polished' to behave in a certain way. They also have a 'creativity' parameter that makes
sure it generates a different answer each time a more complex question is asked. This way they do not give the exact
same answer for every question to every user. (It works somewhat different of course, but I hope you get the picture).&lt;/p&gt;
&lt;p&gt;You can ask a LLM to write a story for you, or just a complete book and it can be very appealing. &lt;a href="https://youtu.be/aZvTmy4gFlM?si=pg-lUuIYoMtyCB6w"&gt;But somehow all
those created texts often feel somewhat 'off'&lt;/a&gt;. The first responses
I got on my articles mentioned on the top of this article are an expression of that. There is even a whole science
evolving around &lt;a href="https://ahrefs.com/blog/how-to-detect-ai-generated-content/"&gt;that&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If I ask a LLM to setup a Python FastAPI project for me, it creates a file structure that works. Very handy, I get a
speed jump starting a new project and that is a benefit for a programmer. But if a whole program or web site is
generated by AI you do not get the creativity needed for that program, you get the average code the LLM was trained on
, usually by getting the code from Github. But all that publicly available code it was trained on, also has &lt;em&gt;A LOT&lt;/em&gt; of
bad code the LLM is also trained on.  &lt;/p&gt;
&lt;p&gt;A LLM basically works like the principe of &lt;a href="https://en.wikipedia.org/wiki/Wisdom_of_the_crowd"&gt;wisdom of the crowd&lt;/a&gt; that generally leads to good results. Never the best result, but good enough.&lt;br&gt;
Is that a bad idea? Probably not, if a company wants to hire the best programmer, it will probably also decide to hire
one that is 'good enough' because the best is probably the most expensive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;By using vibe coding you by definition accept you only get results that are at best 'good enough'.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A human programmer will try to deliver the best in the time he has to accomplish a task. It will probably contain bugs,
it will be a different from the average generated by an AI tool, but the programmer will have thought of it, and probably
has put one or two really smart decisions in the code, and probably kept other (future) requirements
in mind to create it.&lt;br&gt;
It will probably have more use for the company he is writing it for, and thus have higher quality.
Basically, he puts his creativity into it.&lt;/p&gt;
&lt;p&gt;If a writer of a novel writes a book, he &lt;em&gt;wants&lt;/em&gt; his book to be good. Not because is also sells better, but he/she will
be proud of it. If you tweak a LLM to write books that sell the best, they will turn out more and more of the same.&lt;/p&gt;
&lt;h1&gt;Business case of vibe coding tools&lt;/h1&gt;
&lt;p&gt;AI is a hype at the moment. In the beginning of the internet, a lot of Internet Service Providers (ISP) provided free
internet access in which the users became the product. The internet was still small at that time, investments in the
networks were high, and ISP's needed money to grow. So they gave the access to the internet away for free to leverage
maximum growth to get as much users as possible.&lt;br&gt;
The ISP's sold the customer profile data of their customers to advertisement companies which then could build more specific profiles of all those new online users to target ads more efficiently. This made the internet profitable very quickly.&lt;br&gt;
After this initial hype was over, free ISP's basically disappeared and we now all pay for internet access to view ads served by those advertisement companies that have a more and more detailed profile of each person online every day.&lt;/p&gt;
&lt;p&gt;Another example is the cloud computing business. When the large cloud providers started, they only had a minimal viable
product they sold for cheap, even for free. &lt;a href="https://aws.amazon.com/free/"&gt;AWS&lt;/a&gt;, &lt;a href="https://cloud.google.com/free?hl=en"&gt;Google&lt;/a&gt; and &lt;a href="https://azure.microsoft.com/en-in/pricing/free-services/"&gt;Microsoft&lt;/a&gt; still offer free services, just enough to try
something out, but when you really want to use services they offer, you pay more for it now
&lt;a href="https://coloclue.net/en/kosten/" target="_blank"&gt;than hosting it yourself&lt;/a&gt;. &lt;em&gt;Nowadays a lot of businesses fully rely on the cloud, resulting in vendor lock-in&lt;/em&gt; and &lt;a href="https://berthub.eu/articles/posts/beware-cloud-is-part-of-the-software/"&gt;almost no way back&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;AI companies use the same strategy.&lt;/p&gt;
&lt;p&gt;In the beginning, using the LLM's was free, as the use of ChatGPT sky rocketed, more
advanced functionality became paid very quickly. They invest billions of dollars in it, but it not profitable yet
probably for all AI companies. &lt;a href="https://youtu.be/gMmY2fjFOzE?si=5tHLK14xxHRO0-Gm"&gt;AI looks exactly like the Dot-Com Bubble&lt;/a&gt;.
What they now offer is a low price for the services they offer.&lt;/p&gt;
&lt;p&gt;All the AI companies promise their large language models (LLM) will eventually lead to a form of Artificial General
Intelligence (AGI), but it is becoming clearer and clearer these LLM's are &lt;a href="https://youtu.be/mjB6HDot1Uk?si=nnEuCTdKlRtEybv5"&gt;hitting a wall to achieve that&lt;/a&gt;.&lt;br&gt;
All the investments the AI companies now make to achieve this, must become profitable
in the near future, so I am afraid those prices will become much higher in the future.&lt;/p&gt;
&lt;p&gt;As with Vibe coding, AI companies want you to believe &lt;a href="https://www.techradar.com/pro/nvidia-ceo-predicts-the-death-of-coding-jensen-huang-says-ai-will-do-the-work-so-kids-dont-need-to-learn"&gt;coding is not necessary any more&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the same time as AI became more and more a hype, a lot of programmers were losing their jobs. Not only because the
AI was becoming better and better, but by a large part because those &lt;a href="https://qz.com/tech-layoffs-tax-code-trump-section-174-research-development"&gt;engineers were becoming too expensive for the big tech companies&lt;/a&gt; and AI was a better excuse to
blame. Vibe coding tools have also a free tier, but if you really want to accomplish thing you pay by using tokens you
have to buy. &lt;a href="https://www.reddit.com/r/vibecoding/comments/1m6yxq9/the_dark_reality_behind_ai_vibe_coding_money/"&gt;This is a never ending story&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This means less jobs for computer programmers, and the AI companies happily jump into the gap, because companies still need programming roles and AI is now cheap.&lt;/p&gt;
&lt;p&gt;But what if there are no computer programming roles left? What if AI becomes so expensive it will cost more than a programmer costed in 2024? The answer you usually get is that with new technology there is a shift of jobs in the long term. There
are now people needed in new roles like &lt;a href="https://www.linkedin.com/jobs/search?keywords=AI%20Prompting&amp;amp;location=United%20States"&gt;AI prompting roles&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;I have doubts in this case. Vibe coding does not seem the holy grail to me and the AI companies want to replace too much
jobs, not only computer programmers. The costs of a generic business will rise in the long term only benefiting the
few AI big tech companies. Of course I cannot predict the future, but what happened with the large cloud providers and their vendor lock-in makes me nervous.&lt;/p&gt;
&lt;h1&gt;My advice for now&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Be &lt;em&gt;very&lt;/em&gt; careful of giving away &lt;em&gt;your&lt;/em&gt; intellectual property to AI companies, especially the contents of your databases.&lt;/li&gt;
&lt;li&gt;Make use of AI &lt;em&gt;as a tool&lt;/em&gt; to increase productivity at non important parts of your company at least for now, do not rely on vibe coding only yet.&lt;/li&gt;
&lt;/ul&gt;</content><category term="AI, programming"></category><category term="ai"></category><category term="vibe coding"></category><category term="business strategy"></category><category term="programming"></category><category term="intellectual property"></category></entry><entry><title>Python compile problem on Linux as Samba user</title><link href="https://palanthir.nl/python-compile-problem-on-linux-as-samba-user-en.html" rel="alternate"></link><published>2025-06-16T00:00:00+02:00</published><updated>2025-06-16T00:00:00+02:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2025-06-16:/python-compile-problem-on-linux-as-samba-user-en.html</id><summary type="html">&lt;p&gt;Last week I had a head banging problem that had me pulling my hair. Although I do not do it often,
I needed to install a specific version of Python on my laptop. Usually I use &lt;a href="https://asdf-vm.com"&gt;asdf&lt;/a&gt; or &lt;a href="https://github.com/pyenv/pyenv"&gt;py-env&lt;/a&gt; to accomplish this. Both utilities are package managers to install a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last week I had a head banging problem that had me pulling my hair. Although I do not do it often,
I needed to install a specific version of Python on my laptop. Usually I use &lt;a href="https://asdf-vm.com"&gt;asdf&lt;/a&gt; or &lt;a href="https://github.com/pyenv/pyenv"&gt;py-env&lt;/a&gt; to accomplish this. Both utilities are package managers to install a specific version of a package for local use. They do that by compiling the Python version locally and install it in a directory in your home directory to avoid the versions installed system-wide. This makes switching between Python versions for different projects easier.&lt;br&gt;
So I just typed in the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;asdf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Enumerating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="k"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Counting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="k"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Compressing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="k"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reused&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;reused&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Unpacking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;26.01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KiB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MiB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pyenv&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pyenv&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;bf193666&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;e13b5848&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;master&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;master&lt;/span&gt;
&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;PALANTHIR&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;arjan&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;asdf&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;installs&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;Downloading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="n"&gt;tar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xz&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ftp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="n"&gt;tar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xz&lt;/span&gt;
&lt;span class="n"&gt;Installing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;

&lt;span class="n"&gt;BUILD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FAILED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;openSUSE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20250611&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.6&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ge13b5848&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Inspect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;working&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;20250616083639.305007&lt;/span&gt;
&lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;logged&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;20250616083639.305007&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;

&lt;span class="n"&gt;Last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5294&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;
&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5294&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;
&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5294&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;
&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5294&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;
&lt;span class="n"&gt;checking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;whether&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compiler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;works&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5341&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;
&lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5386&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;
&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;20250616083639.305007&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&lt;/span&gt;
&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compiler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cannot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;executables&lt;/span&gt;
&lt;span class="n"&gt;See&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; for more details&lt;/span&gt;
&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;installing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can see the command downloads the specific Python version, tries to compile it, and fails. Usually when a compile fails on Linux there is something wrong with your local environment, so I spent hours checking what was wrong with my local environment until I noticed that the command just worked perfectly when run as a local user on my laptop.&lt;br&gt;
Upon investigating what exactly went wrong I started to notice the command had issues during the configure process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5317&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;checking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;whether&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compiler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;works&lt;/span&gt;
&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5339&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gcc&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="sr"&gt;/home/PALANTHIR/arjan/.asdf/installs/python/3.13.2/include -L/home/PALANTHIR/arjan/.asdf/installs/python/3.13.2/lib -Wl,-rpath,/home/PALANTHIR/arjan/.asdf/installs/python/3.13.2/lib conftest.c -L/home/PALANTHIR/arjan/.asdf/installs/python/3.13.2/lib -Wl,-rpath,/home/PALANTHIR/arjan/.asdf/installs/python/3.13.2/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5343&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;$&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5383&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;program&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5388&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="sr"&gt;/tmp/python-build.20250616083639.305007/&lt;/span&gt;&lt;span class="n"&gt;Python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&lt;/span&gt;
&lt;span class="s1"&gt;configure:5390: error: C compiler cannot create executables&lt;/span&gt;
&lt;span class="s1"&gt;See `config.log&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;the line 5294 as mentioned above in the error description on the configure script does this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cat conftest.er1 &amp;gt;&amp;amp;5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since the error "Bad file descriptor" usually means a file does not exist, it HAD to be the part of the command "&amp;gt;&amp;amp;5" which basically means sending the output to a specific file stream used by configure. This has nothing to do with a local environment. So I started searching on the internet if someone already had this problem. After hours of searching I finally found a hint: the problem arises when the user is a Samba user and indeed my Linux installs are joined in an ActiveDirectory domain using &lt;code&gt;winbind&lt;/code&gt;, which is part of the &lt;code&gt;Samba&lt;/code&gt; package.&lt;br&gt;
And then I found a &lt;a href="https://bugzilla.samba.org/show_bug.cgi?id=15464"&gt;bug in Samba version 4.18&lt;/a&gt;. But I am using Samba 4.22 so the &lt;em&gt;bug apparently is back&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I tried to test it. I changed one line in the &lt;code&gt;/etc/nsswitch.conf&lt;/code&gt; file from:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;passwd&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;compat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;winbind&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;to&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;passwd&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;compat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and then tried again, and it worked!&lt;/p&gt;
&lt;p&gt;By this time it was 3 AM in the night. Since I do not have an account on the Samba Bugzilla site I could not leave a comment on this bug to re-open it. I have sent the Samba team an email to notify them of my find. I will update this page if I hear back from them.&lt;/p&gt;
&lt;p&gt;For me, this is really an edge case of what can happen with software. Because it was so late in the night I did not bother to investigate what is causing this bug. It could however do it if I had the time. This is what I like about open source software.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Update 2025-06-16 14:04&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Samba team decided to give me a user account on their Bugzilla, so I updated the bug report there also &lt;a href="https://bugzilla.samba.org/show_bug.cgi?id=15646#c1"&gt;samba-4.18.0 and above causes pyenv to fail with &lt;code&gt;Bad file descriptor&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;</content><category term="linux"></category><category term="linux"></category><category term="python"></category><category term="asdf"></category><category term="py-env"></category><category term="samba"></category><category term="winbind"></category></entry><entry><title>Tracking history in database data</title><link href="https://palanthir.nl/tracking-history-in-database-data-en.html" rel="alternate"></link><published>2025-05-27T00:00:00+02:00</published><updated>2025-05-27T00:00:00+02:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2025-05-27:/tracking-history-in-database-data-en.html</id><summary type="html">&lt;p&gt;This week I noticed strange behaviour in a system containing a table that is pretty straight forward and does
not have any history logging, versioning or anything like that. The contents of the table are updated by automatic
processes without logging also. Since two weeks there have been updates to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This week I noticed strange behaviour in a system containing a table that is pretty straight forward and does
not have any history logging, versioning or anything like that. The contents of the table are updated by automatic
processes without logging also. Since two weeks there have been updates to the contents of this table which I 
did not notice in the years before. At such moments it is difficult to track where the updates come from or maybe who
updated it manually. This situation always results in guessing what happened. In the end I restored the contents of the
table from 2 weeks ago (thank you backups!) but this is generally not ideal.&lt;/p&gt;
&lt;p&gt;History in database data in my opinion is about keeping track of changes of database records. 
For example, you want to keep track of when and by whom a database record was updated. This is the case 
when you do not want to lose earlier data or want to be able to see what the value was of a specific
database record in the past. Most importantly, if a record is deleted from the database, you will never again know
what the contents was of that specific database record.&lt;/p&gt;
&lt;p&gt;This can be implemented for any database type and product, but there
are differences how you can choose to implement them. In practice I see basically 
two variants of how to implement this. As I am mostly working with
relational databases, let's take a moment to look at those variants:&lt;/p&gt;
&lt;h3&gt;Versioning table&lt;/h3&gt;
&lt;p&gt;Write a copy of a record to another table every time a modification on a record is executed.
This is handled by code or database triggers that store a full copy of 
the database record in another table.&lt;br&gt;
An example like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;bookname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DATETIME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;versioned_by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;bookname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note, SQL commands in this article are not meant to directly execute on a database. They are solely for explanatory purposes to indicate the subject.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this example we have a table book, and just before a mutation of a book is written to the database, a copy of the old record
is stored in the book_version, along with the timestamp and name of the user that updated the book.
A lot of Object - Relation Mapping (ORM) libraries for different languages support this kind of history keeping.&lt;br&gt;
The usual history flow of a database record in the table book happens as follows:
1. When a new book is added to the database, an SQL INSERT command is used.&lt;br&gt;
Some ORM libraries add also an initial version to the book_version table, but not all.
2. When a book is updated, just before issuing the SQL UPDATE command, this happens:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;versioned_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bookname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;username&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bookname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Step 2 also happens when a book is removed from the &lt;code&gt;book&lt;/code&gt; table before executing the DELETE command.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This way you can very easily get a full history of records in a database.&lt;/p&gt;
&lt;h4&gt;Advantages&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Minimal overhead.&lt;/li&gt;
&lt;li&gt;History is separated from the current version of the data.&lt;/li&gt;
&lt;li&gt;Better normalization of data.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Disadvantages&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;The only way to detect if a record was deleted from the table &lt;code&gt;book&lt;/code&gt; is to examine the &lt;code&gt;book_history&lt;/code&gt; table.
Only if the book_id is present in the &lt;code&gt;book_history&lt;/code&gt; table and not in the &lt;code&gt;book&lt;/code&gt; table, can you determine this record was present
in the table at some moment.&lt;/li&gt;
&lt;li&gt;While you can see the old values of the records in the &lt;code&gt;book_history&lt;/code&gt; table, it is more difficult to compare the versioned
records to the current state of the record. You have to write a join between the tables to select both current and past data.
In short: the normalization is a disadvantage in this case.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Keeping track of modifications in the same table&lt;/h3&gt;
&lt;p&gt;Basically, this variant adds timestamp columns to the table for each record:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;objectid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;bookname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;added_dt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;added_by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;modified_dt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;modified_by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;removal_dt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;removal_by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Now, every time a record is inserted, only the &lt;code&gt;added_dt&lt;/code&gt; and &lt;code&gt;added_by&lt;/code&gt; columns are filled (along with the content columns).  &lt;/li&gt;
&lt;li&gt;For updates to the record, two things happen:&lt;ol&gt;
&lt;li&gt;Mark the old record as removed by setting the &lt;code&gt;removal_dt&lt;/code&gt; and &lt;code&gt;removal_by&lt;/code&gt; columns.&lt;/li&gt;
&lt;li&gt;Insert a new record based on the existing record with the new values, and by also updating the &lt;code&gt;modified_dt&lt;/code&gt; and &lt;code&gt;modified_by&lt;/code&gt; columns.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Deleting a record also means updating the &lt;code&gt;removal_dt&lt;/code&gt; and &lt;code&gt;removal_by&lt;/code&gt; columns.
This way, the most recent data of the records is always available by using a SELECT query like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;removal_dt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you want to see how the changes of the book progressed over time, just write a query like this without the use of the &lt;code&gt;removal_dt&lt;/code&gt; column in the WHERE clause:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objectid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which gives a result like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;objectid&lt;/th&gt;
&lt;th&gt;book_id&lt;/th&gt;
&lt;th&gt;bookname&lt;/th&gt;
&lt;th&gt;author&lt;/th&gt;
&lt;th&gt;added_dt&lt;/th&gt;
&lt;th&gt;added_by&lt;/th&gt;
&lt;th&gt;modified_dt&lt;/th&gt;
&lt;th&gt;modified_by&lt;/th&gt;
&lt;th&gt;removal_dt&lt;/th&gt;
&lt;th&gt;removal_by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The fellowship of the Ring&lt;/td&gt;
&lt;td&gt;Tolkien&lt;/td&gt;
&lt;td&gt;2025-05-27 08:00:00&lt;/td&gt;
&lt;td&gt;Arjan van der Veen&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;2025-05-27 08:00:15&lt;/td&gt;
&lt;td&gt;Arjan van der Veen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The fellowship of the Ring&lt;/td&gt;
&lt;td&gt;J.R.R. Tolkien&lt;/td&gt;
&lt;td&gt;2025-05-27 08:00:00&lt;/td&gt;
&lt;td&gt;Arjan van der Veen&lt;/td&gt;
&lt;td&gt;2025-05-27 08:00:15&lt;/td&gt;
&lt;td&gt;Arjan van der Veen&lt;/td&gt;
&lt;td&gt;2025-05-27 08:00:30&lt;/td&gt;
&lt;td&gt;Arjan van der Veen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The Fellowship of the Ring&lt;/td&gt;
&lt;td&gt;J.R.R. Tolkien&lt;/td&gt;
&lt;td&gt;2025-05-27 08:00:00&lt;/td&gt;
&lt;td&gt;Arjan van der Veen&lt;/td&gt;
&lt;td&gt;2025-05-27 08:00:30&lt;/td&gt;
&lt;td&gt;Arjan van der Veen&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Here I added "The Fellowship of the Ring" to the table, and updated it a few seconds later by correcting the name of the author and then the title.&lt;/p&gt;
&lt;h4&gt;Advantages&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Minimal overhead.&lt;/li&gt;
&lt;li&gt;History is stored within the same table, so it is easier to see what changed over time.&lt;/li&gt;
&lt;li&gt;Deleted records are still visible for a DBA.&lt;/li&gt;
&lt;li&gt;The latest change is always visible with the current record (in this case record with &lt;code&gt;objectid&lt;/code&gt; 3).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Disadvantages&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;You have to extend WHERE clauses of your SQL statements everywhere to use the &lt;code&gt;removal_dt IS NULL&lt;/code&gt; to get your current data. 
This can easily be solved by using a database view for this table. But since almost all SQL queries have a WHERE clause this is no issue for me personally.&lt;/li&gt;
&lt;li&gt;More complex to use this database design with an ORM (Object - Relation Mapping) library when coding.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the past I wrote an extension to Java Hibernate ORM to automatically make use of this pattern for a client, but I do not have
the license to share that code. It is doable though to implement it and significantly makes coding an application using this
easier. Nowadays I just write SQL commands directly and noticed that, for me personally, it does not take much longer to develop applications with this database design.&lt;/p&gt;</content><category term="databases"></category><category term="sql"></category><category term="databases"></category><category term="history"></category><category term="orm"></category><category term="database design"></category></entry><entry><title>Vernieuwde website</title><link href="https://palanthir.nl/vernieuwde-website.html" rel="alternate"></link><published>2025-03-04T15:20:00+01:00</published><updated>2025-03-04T15:20:00+01:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2025-03-04:/vernieuwde-website.html</id><summary type="html">&lt;p&gt;Na een algemene website gehad te hebben met eigenlijk alleen een CV er op is het tijd voor een vernieuwing.
Meer dan 20 jaar geleden was de site ongewijzigd en de tijden zijn veranderd. Op deze vernieuwde site kan je
nog steeds &lt;a href="https://palanthir.nl/pages/cv.html"&gt;mijn CV&lt;/a&gt; vinden als oprichter van Palanthir, maar …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Na een algemene website gehad te hebben met eigenlijk alleen een CV er op is het tijd voor een vernieuwing.
Meer dan 20 jaar geleden was de site ongewijzigd en de tijden zijn veranderd. Op deze vernieuwde site kan je
nog steeds &lt;a href="https://palanthir.nl/pages/cv.html"&gt;mijn CV&lt;/a&gt; vinden als oprichter van Palanthir, maar ook artikelen over technische
IT zaken, of meer om kennis te delen over uiteenlopende zaken.&lt;/p&gt;</content><category term="info"></category></entry><entry><title>Scaling KDE apps on Gnome Wayland</title><link href="https://palanthir.nl/scaling-kde-apps-on-gnome-wayland-en.html" rel="alternate"></link><published>2025-03-01T16:30:00+01:00</published><updated>2025-03-01T16:30:00+01:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2025-03-01:/scaling-kde-apps-on-gnome-wayland-en.html</id><summary type="html">&lt;p&gt;When using Gnome on Linux with Wayland in combination with font scaling using Gnome Tweaks, KDE apps are not scaled accordingly.&lt;br&gt;
One solution is to set an environment variable in &lt;code&gt;~/.config/environment.d/kdeapps.conf&lt;/code&gt; containing the scale factor for KDE apps like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;QT_SCALE_FACTOR=1.3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Reboot, and after …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When using Gnome on Linux with Wayland in combination with font scaling using Gnome Tweaks, KDE apps are not scaled accordingly.&lt;br&gt;
One solution is to set an environment variable in &lt;code&gt;~/.config/environment.d/kdeapps.conf&lt;/code&gt; containing the scale factor for KDE apps like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;QT_SCALE_FACTOR=1.3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Reboot, and after that the KDE apps are scaled accordingly.&lt;/p&gt;</content><category term="linux"></category><category term="linux"></category><category term="gnome"></category><category term="wayland"></category><category term="scaling"></category><category term="KDE apps"></category></entry><entry><title>IPv6 NAT on libvirt virtual machine</title><link href="https://palanthir.nl/ipv6-nat-on-libvirt-virtual-machine-en.html" rel="alternate"></link><published>2023-12-15T00:00:00+01:00</published><updated>2023-12-15T00:00:00+01:00</updated><author><name>Arjan van der Veen</name></author><id>tag:palanthir.nl,2023-12-15:/ipv6-nat-on-libvirt-virtual-machine-en.html</id><summary type="html">&lt;p&gt;librtvirt supports IPv6 for virtual machines, but that is usually a bridge solution or a subnet of the /64 the libvirt host is connected to.
But I'm in the situation working on a network without IPv6, so I get my IPv6 address from a Wireguard interface.
To get IPv6 working …&lt;/p&gt;</summary><content type="html">&lt;p&gt;librtvirt supports IPv6 for virtual machines, but that is usually a bridge solution or a subnet of the /64 the libvirt host is connected to.
But I'm in the situation working on a network without IPv6, so I get my IPv6 address from a Wireguard interface.
To get IPv6 working in a virtual machine on my host, I do not have a range available on my host machine without extensive changes to the wireguard VPN.  &lt;/p&gt;
&lt;p&gt;The solution is to use NAT with IPv6.&lt;/p&gt;
&lt;p&gt;First, create a new libvirt NAT network with IPv6 enabled and the ranges you want. For the ipv6 range I chose fd00::/64 because that is a private range like 10.x.x.x.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;network&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;connections=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;name&amp;gt;&lt;/span&gt;virbr1&lt;span class="nt"&gt;&amp;lt;/name&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;uuid&amp;gt;&lt;/span&gt;a592c6e0-b9ed-463a-a0a3-3feef8c01b7d&lt;span class="nt"&gt;&amp;lt;/uuid&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;forward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;mode=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;nat&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;nat&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;start=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;1024&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;end=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;65535&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/nat&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/forward&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;bridge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;virbr1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;stp=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;on&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;delay=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;mac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;address=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;52:54:00:74:c6:cf&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;virbr1&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;address=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;192.168.100.1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;netmask=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;255.255.255.0&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;dhcp&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;start=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;192.168.100.128&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;end=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;192.168.100.254&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/dhcp&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/ip&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;family=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;ipv6&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;address=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fd00::1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;prefix=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;64&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;dhcp&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;start=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fd00::100&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;end=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fd00::1ff&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/dhcp&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/ip&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/network&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Because we want to do nat and I could not find a libvirt network filter rule for doing nat, I used the hook mechanism of libvirt, which means putting a script called &lt;code&gt;network&lt;/code&gt; in &lt;code&gt;/etc/libvirt/hooks&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;VIRT_NET_MATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;virbr1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;VIRT_BRIDGE_MATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;virbr1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;IPV6_DEFAULT_INTERFACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vpn-thuis&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VIRT_NET_MATCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;VIRT_BRIDGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$VIRT_BRIDGE_MATCH&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;started&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Adding NAT MASQUERADING entries for &lt;/span&gt;&lt;span class="nv"&gt;$VIRT_BRIDGE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ip6tables&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;nat&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;POSTROUTING&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;fd00::/64&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$IPV6_DEFAULT_INTERFACE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;MASQUERADE
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;stopped&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Stopping &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$VIRT_BRIDGE&lt;/span&gt;&lt;span class="s2"&gt;) ...&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ip6tables&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;nat&lt;span class="w"&gt; &lt;/span&gt;-D&lt;span class="w"&gt; &lt;/span&gt;POSTROUTING&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;fd00::/64&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$IPV6_DEFAULT_INTERFACE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;MASQUERADE
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Make sure this file is executable.
&lt;code&gt;IPV6_DEFAULT_INTERFACE&lt;/code&gt; is the mail IPv6 interface over which the natting should take place.
The rest is self explanatory I think. When libvirt brings the network interface online, it calls the &lt;code&gt;/etc/libvirt/hooks/network&lt;/code&gt; script, and this handled the creation or deleten of the NAT rule in the firewall of the host.&lt;/p&gt;</content><category term="linux"></category><category term="linux"></category><category term="ipv6"></category><category term="network"></category></entry></feed>